Modelagem Preditiva - Simulação de Monte Carlo e Séries Temporais

Modelagem Ativo Financeiro Vale3

Portifólio Thiago Gonçalves Custódio

1. Definição do Problema

Através de modelagem preditiva financeira prever comportamento para os próximos dias do Ativo Financeiro Vale3.

1.1 Informações sobre o DataSet

2. Instalando e Carregando os Pacotes

In [1]:
!pip install -q -U watermark

# Imports para manipulação de dados
import numpy as np
import pandas as pd

# Imports para visualização
import matplotlib.pyplot as plt
import matplotlib as m
import seaborn as sns
import plotly.graph_objects as go

# Imports para cálculos estatísticos
import scipy
from scipy.stats import kurtosis, skew, shapiro
import warnings
warnings.filterwarnings("ignore")

# Imports para formatação dos gráficos
plt.style.use('fivethirtyeight')
m.rcParams['axes.labelsize'] = 14
m.rcParams['xtick.labelsize'] = 12
m.rcParams['ytick.labelsize'] = 12
m.rcParams['text.color'] = 'k'
from matplotlib.pylab import rcParams 
rcParams['figure.figsize'] = 20,10


# Versões dos pacotes usados neste jupyter notebook
%reload_ext watermark
%watermark -a "Thiago Gonçalves Custódio" --iversions
Author: Thiago Gonçalves Custódio

scipy     : 1.6.2
numpy     : 1.20.1
seaborn   : 0.11.1
plotly    : 4.14.3
matplotlib: 3.4.0
pandas    : 1.2.2

3. Carga dos Dados / DataSet

In [2]:
# Carrega o dataset
dados = pd.read_excel("dados/Vale3.xlsx", parse_dates = True, index_col = "Date")
In [3]:
# Visualizando registros
# Cada coluna representa o valor da ação em cada dia da série.
# valor de abertura, fechamento, máximo, mínimo e volume. 
# A coluna Mudanca(%) representa a variação diária.
dados.head()
Out[3]:
Abertura Máxima Mínima Fechamento
Date
2021-11-30 69.00 71.47 68.69 69.43
2021-11-29 70.28 70.60 69.16 69.50
2021-11-26 67.50 69.36 67.50 68.64
2021-11-25 71.29 71.29 69.80 70.50
2021-11-24 70.11 70.98 69.51 70.98
In [4]:
# Tipos de Dados
dados.dtypes
Out[4]:
Abertura      float64
Máxima        float64
Mínima        float64
Fechamento    float64
dtype: object
In [5]:
# Shape
dados.shape
Out[5]:
(6633, 4)
In [6]:
# Sumário estatístico
dados.describe()
Out[6]:
Abertura Máxima Mínima Fechamento
count 6633.000000 6633.000000 6633.000000 6633.000000
mean 18.410410 18.668820 18.135338 18.399596
std 19.146923 19.376753 18.885156 19.127750
min 0.210000 0.220000 0.200000 0.200000
25% 1.710000 1.720000 1.700000 1.720000
50% 14.610000 14.970000 14.310000 14.620000
75% 26.340000 26.570000 25.930000 26.290000
max 106.550000 107.130000 104.520000 106.380000

4. Análise Exploratória

4.1 Visualizando o Preço Diário dos Ativo no Tempo

In [7]:
# Figura
fig = go.Figure(data = [go.Candlestick(x = dados.index,
                                       open = dados['Abertura'],
                                       high = dados['Máxima'],
                                       low = dados['Mínima'],
                                       close = dados['Fechamento'])])

# Layout
fig.update_layout(title = "Candlestick Series do Preço de Ações da Empresa Vale3 ")

# Gráfico
fig.show()

4.2 Visualizando o Preço Diário de Fechamento das Ações no Tempo

In [8]:
# Plot
plt.plot(dados["Fechamento"])
plt.title("Preço Diário de Fechamento das Ações", size = 14)
plt.show()

Calculo do retorno diário da série.

In [9]:
# Calculando o percentual de mudança na cotação de fechamento diário das ações
# Ou seja, quanto o valor de fechamento varia de um dia para outro, o retorno diário da ação
retorno_diario = dados["Fechamento"].pct_change().dropna()
retorno_diario.head()
Out[9]:
Date
2021-11-29    0.001008
2021-11-26   -0.012374
2021-11-25    0.027098
2021-11-24    0.006809
2021-11-23   -0.022682
Name: Fechamento, dtype: float64

Calculo do retorno acumulado da série.

In [10]:
# Retorno acumulado
retorno_diario_acumulado = (1 + retorno_diario).cumprod() - 1
retorno_diario_acumulado.max()
Out[10]:
0.5321906956646989

4.3 Estatística Descritiva

Vamos usar a estatística para calcular o retorno médio e a variação (desvio padrão).

In [11]:
# Média do fechamento diário da cotação das ações
media_retorno_diario = np.mean(retorno_diario)
In [12]:
# Desvio padrão do fechamento diário da cotação das ações
desvio_retorno_diario = np.std(retorno_diario)
In [13]:
# Média e desvio padrão
print("Média do Retorno de Fechamento:", media_retorno_diario)
print("Desvio Padrão do Retorno de Fechamento:", desvio_retorno_diario)
Média do Retorno de Fechamento: -0.0004445583264931016
Desvio Padrão do Retorno de Fechamento: 0.028891146966726983

Vamos considerar o ano com 252 dias de funcionamento da bolsa brasileira

In [14]:
# Média e desvio padrão no ano (considerando 252 dias úteis de operações na bolsa americana)
print("Retorno Médio Anualizado de Fechamento:", (1 + media_retorno_diario) ** 252 - 1)
print("Desvio Padrão Anualizado de Fechamento:", desvio_retorno_diario*np.sqrt(252))
Retorno Médio Anualizado de Fechamento: -0.10600366851752219
Desvio Padrão Anualizado de Fechamento: 0.45863273979226615

Retorno médio anualizado mostrou-se negativo.

In [15]:
# Plot
plt.plot(retorno_diario)
plt.title("Retorno Diário", size = 14)
plt.show()
In [16]:
# Plot
plt.hist(retorno_diario, bins = 75)
plt.title("Histograma do Retorno Diário", size = 14)
plt.show()

Os valores estão bem próximos mesmo da média. Mas vamos confirmar isso calculando curtose e assimetria.

In [17]:
# Calculando curtose e assimetria
print("Curtose:", kurtosis(retorno_diario))
print("Assimetria:", skew(retorno_diario))
Curtose: 10.545229612687818
Assimetria: 0.4541327332887858

A curtose indica que os registros estão bem próximos da média. Mas a assimetria indica que os dados podem estar distorcidos e distantes de uma distribuição normal. Vamos aplicar o teste de normalidade na série.

4.4 Teste de Normalidade Shapiro-Wilk

In [18]:
# Executa o teste de normalidade para a série
teste_normalidade = shapiro(retorno_diario)[1]

# Verifica o retorno com base no valor-p de 0.05
if teste_normalidade <= 0.05:
    print("Rejeitamos a Hipótese Nula de Normalidade dos Dados.")
else:
    print("Falhamos em Rejeitar a Hipótese Nula de Normalidade dos Dados.")
Rejeitamos a Hipótese Nula de Normalidade dos Dados.

A distribuição não é normal. Vamos aplicar uma transformação de log à série e então aplicar a técnica de diferenciação para retirar da série os padrões de tendência e deixarmos apenas os dados reais, que nos interessam. Com isso calculamos o retorno diário.

In [19]:
# Transformação de log e diferenciação para cálculo do retorno diário
log_retorno_diario = (np.log(dados["Fechamento"]) - np.log(dados["Fechamento"]).shift(-1)).dropna()

# Calculamos média e desvio padrão após a transformação
log_media_retorno_diario = np.mean(log_retorno_diario)
log_desvio_retorno_diario = np.std(log_retorno_diario)

Vamos criar um plot com o retorno diário da série transformada.

In [20]:
# Plot
plt.plot(log_retorno_diario)
plt.title("Retorno Diário (Log Transformation)", size = 14)
plt.show()
In [21]:
# Plot
plt.hist(log_retorno_diario, bins = 75)
plt.title("Histograma do Retorno Diário (Log Transformation)", size = 14)
plt.show()
In [22]:
# Calculando curtose e assimetria
print("Curtose:", kurtosis(log_retorno_diario))
print("Assimetria:", skew(log_retorno_diario))
Curtose: 9.748101166417717
Assimetria: 0.05783569302111384
In [23]:
# Executa o teste de normalidade para a série
teste_normalidade = shapiro(log_retorno_diario)[1]

# Verifica o retorno com base no valor-p de 0.05
if teste_normalidade <= 0.05:
    print("Rejeitamos a Hipótese Nula de Normalidade dos Dados.")
else:
    print("Falhamos em Rejeitar a Hipótese Nula de Normalidade dos Dados.")
Rejeitamos a Hipótese Nula de Normalidade dos Dados.

Os dados ainda não estão normais, porém reduzimos significativamente a distorção dos dados. Poderíamos aplicar outras transformações, mas para o objetivo este estudo isso é suficiente. Seguimos com a série transformada.

4.5 Valor Histórico

Vamos calcular o valor histórico do preço da ação.

In [24]:
# Nível de variância
var_level = 95
var = np.percentile(log_retorno_diario, 100 - var_level)
print("Certeza de que as perdas diárias não excederão o VaR% em um determinado dia com base em valores históricos.")
print("VaR 95%:", var)
Certeza de que as perdas diárias não excederão o VaR% em um determinado dia com base em valores históricos.
VaR 95%: -0.04125836570265247
In [25]:
# Var para os próximos 5 dias
var * np.sqrt(5)
Out[25]:
-0.0922565103516768

4.6 Valor Histórico Condicional

In [26]:
# Nível de variância
var_level = 95
var = np.percentile(log_retorno_diario, 100 - var_level)
cvar = log_retorno_diario[log_retorno_diario < var].mean()
print("Nos piores 5% dos casos, as perdas foram, em média, superiores ao percentual histórico.")
print("CVaR 95%:", cvar)
Nos piores 5% dos casos, as perdas foram, em média, superiores ao percentual histórico.
CVaR 95%: -0.06499672338281128

5. Monte Carlo simulation

In [27]:
# Simulação de Monte Carlo

# Número de dias a frente
dias_posteriores = 252

# Número de simulações
simulacoes = 2500

# Último valor da ação
ultimo_preco = 69.43

# Cria um array vazio com as dimensões 
results = np.empty((simulacoes, dias_posteriores))

# Loop por cada simulação
for s in range(simulacoes):
    
    # Calcula o retorno com dados randômicos seguindo uma distribuição normal
    random_returns = 1 + np.random.normal(loc = log_media_retorno_diario, 
                                          scale = log_desvio_retorno_diario, 
                                          size = dias_posteriores)
    
    result = ultimo_preco * (random_returns.cumprod())
    
    results[s, :] = result
In [28]:
# Definindo o índice da série simulada
index = pd.date_range("2021-12-01", periods = dias_posteriores, freq = "D")
resultados = pd.DataFrame(results.T, index = index)
media_resultados = resultados.apply("mean", axis = 1)
In [29]:
# Dividindo a área de plotagem em 2 subplots
fig, ax = plt.subplots(nrows = 2, ncols = 1)

# Plot
ax[0].plot(dados["Fechamento"][:"2018-12-31"])

ax[0].plot(resultados)

ax[0].axhline(69.43, c = "orange")

ax[0].set_title(f"Monte Carlo {simulacoes} Simulações", size = 14)

ax[0].legend(["Preço Histórico", "Último Preço = 69.43"])

ax[1].plot(dados["Fechamento"][:"2018-12-31"])

ax[1].plot(resultados.apply("mean", axis = 1), lw = 2)

ax[1].plot(media_resultados.apply((lambda x: x * (1+1.96 * log_desvio_retorno_diario))), 
           lw = 2, linestyle = "dotted", c = "gray")

ax[1].plot(media_resultados, lw = 2, c = "orange")

ax[1].plot(media_resultados.apply((lambda x: x * (1-1.96 * log_desvio_retorno_diario))), 
           lw = 2, linestyle = "dotted", c = "gray")

ax[1].set_title(f"Resultado Médio Monte Carlo {simulacoes} Simulações", size = 14)

ax[1].legend(["Preço", "Previsão Média", "2x Desvio Padrao"])

plt.show()

6. Conclusão

A previsão é positiva com os dados simulados e no longo prazo as ações da VALE3 tendem a valorizar.

Observação importante: Não sou analista de valores mobiliários, a analise acima reflete apenas a minha opinião pessoal baseada nos estudos neste projeto realizados na respectiva data de sua elaboração.

Entre em Contato!

Caso tenha alguma dúvida ou sugestão, entre em contato.

Github Linkedin E-mail Portfólio